This document is Copyright (c) 1995 by Bradley Beacham. All rights reserved. I encourage you to copy and distribute it, so long as you leave it unchanged. It may NOT be used for commercial purposes without my explicit prior permission.
I welcome any comments, questions, additional information and corrections. My addresses are:
CompuServe: 74223,2745 Internet : 74223.2745@compuserve.com Post : Bradley Beacham 1343 S. Tyler St. Salt Lake City, Utah 84105-2122 U.S.A.
Most importantly, thanks to Bert Tyler, the original creator of Fractint, and to Timothy Wegner and the rest of the Stone Soup Group for doing so much to improve it. Special thanks also to Mark Peterson for creating the original formula parser, and to Chuck Ebbert for making it go so much faster. A big thank you to all of the formula authors who have taught me so much through their examples, particularly Jonathan Osuch.
A big thank-you to Ronald Black. Ron's excellent questions and thoughtful critiques have made this document much better than it would have been otherwise. Thanks also to Bob Carr, Jon Horner, Dan Parchman, David Walter and Lee Skinner (and others already mentioned) for making the effort to read preliminary versions of this document, and for offering insights, suggestions and encouragement.
And finally, an unsolicited plug: If you don't already have a copy of FRACTAL CREATIONS by Timothy Wegner and Bert Tyler, get one. It covers much of the same material as this document, with the advantage of being written by the people most responsible for the development of Fractint. It also includes *lots* of stuff that isn't covered here. Several weeks after completing the first drafts of this document, I reread the book and realized that some parts of this tutorial are simply paraphrases of passages from FRACTAL CREATIONS; although I wasn't consciously imitating the book, the debt is obvious. In this case, imitation really *is* the sincerest form of flattery.
Except for quoted material, this document was not written by a Fractint programmer. I am a self-taught enthusiast, not a wizard. Consequently, you may find yourself disagreeing with my material, my conclusions, or my approach to the subject. If so, I look forward to hearing from you. It may be that we will end up disagreeing, but I am certainly willing to hear your critique. If you think that something needs to be added or corrected, please contact me and I'll attempt to fix the problems in a future version.
Speaking of versions, bear in mind that I am using Fractint for DOS, version 19.0. Other versions of Fractint may render some of this material obsolete or inapplicable.
Please don't be intimidated by the length of the document. You don't *need* to understand it all before you can get started and have a lot of fun. If you find it verbose, let me paraphrase Abraham Lincoln: "Sorry it's so long. If I had more time, it would have been much shorter."
A word of warning for readers in the USA: I have adopted the European convention of placing trailing punctuation *outside* a quote. This seems especially appropriate when dealing with literal strings processed by the computer, but it also just makes more sense to me. I hope it doesn't bother you.
I wrote this in an attempt to fill a perceived gap. While much has been written about the Fractint program, the formula parser is still something of a mystery to some. Many people use the parser to create beautiful fractal images, but some (most?) have never even attempted to write a formula of their own.
There are many possible reasons for this, but I believe one of the most pertinent is that the documentation on this subject in the standard Fractint package is rather terse. (This documentation is reproduced in section 5.0.) It provides important information, but it didn't exactly leave me with the feeling that I knew what to do next.
Luckily for me, I plunged in anyway, and discovered that I could create my own formulas and have a wonderful time doing it. Most of my time spent with Fractint, by far, is devoted to fooling around with the formula parser. It's always exciting to create an interesting new image, but it's at least twice as satisfying to me when I find a beautiful image, full of wonderful complexity and chaos and order, coming from a formula that I wrote. And I do this despite the fact that I am *not* a highly trained mathematician.
You really don't need to be a math wizard to write a Fractint formula! (Although if you are, so much the better.) All you really need is patience, persistence, and the willingness to learn.
I learned a lot by reading FRACTAL CREATIONS. I also learned by reading files of formulas written by other people, trying to trace through the logic of the formulas and understand what they were doing. And some of what I learned came from simple experimentation and trial-and-error.
Now I will try to summarize the most important things that I have learned so far. I hope this information will help potential formula authors get started. If I am successful, you will be spared much of the head-scratching that I endured along the way.
I am assuming that you have a copy of the Fractint program, and that you have used it enough to know how to choose a formula from the menu system. I also assume that you want to try writing your own formulas.
It is a part of the excellent fractal-generating program, Fractint. While Fractint has many different types of fractal formulas built into it, the formula parser allows you to add new fractals without having to change the program. These formulas are stored in simple text files, and may be viewed and edited by the user.
The standard Fractint package supplies a file of formulas, FRACTINT.FRM, but other formula files may be used. For example, this document discusses many sample formulas which are found in the accompanying file, FRMTUTOR.FRM, and many other formula files (identified by the extension ".frm") may be downloaded from online services or BBSs.
If you have an additional formula file, you should put it in the same directory that holds FRACTINT.FRM. To access it, choose the FORMULA type from the Fractint menu, and then hit the F6 key. You'll be shown a menu of available formula files; select the one you want and then you'll be able to use its formulas.
This document refers to a few other formula files: FRACT001.FRM, BUILTN.FRM, FUBAR.FRM, OVERKILL.FRM and INANDOUT.FRM are available on CompuServe and other online services and BBSs. You won't need any of those files to follow the discussion, however.
In addition to using formula files by other people, of course, there is another way to add new ones: WRITE YOUR OWN!
The "how" is simple: All you need is a simple text-editor, such as the Edit program that comes with MS-DOS or the Notepad program that comes with Windows. Just be sure that your editor saves the file as simple unformatted ASCII text. Then follow the basic rules outlined below.
The "why" is even simpler: Because it's fun.
First, let's look at what Fractint.doc says about formulas: [BEGIN EXCERPT] (type=formula)
This is a "roll-your-own" fractal interpreter - you don't even need a compiler!
To run a "type=formula" fractal, you first need a text file containing formulas (there's a sample file - FRACTINT.FRM - included with this distribution). When you select the "formula" fractal type, Fractint scans the current formula file (default is FRACTINT.FRM) for formulas, then prompts you for the formula name you wish to run. After prompting for any parameters, the formula is parsed for syntax errors and then the fractal is generated. If you want to use a different formula file, press <F6> when you are prompted to select a formula name.
There are two command-line options that work with type=formula ("formulafile=" and "formulaname="), useful when you are using this fractal type in batch mode.
The following documentation is supplied by Mark Peterson, who wrote the formula interpreter:
Formula fractals allow you to create your own fractal formulas. The general format is:
Mandelbrot(XAXIS) { z = Pixel: z = sqr(z) + pixel, |z| <= 4 } | | | | | Name Symmetry Initial Iteration Bailout Condition Criteria
Initial conditions are set, then the iterations performed while the bailout criteria remains true or until 'z' turns into a periodic loop. All variables are created automatically by their usage and treated as complex. If you declare 'v = 2' then the variable 'v' is treated as a complex with an imaginary value of zero.
Predefined Variables (x, y) -------------------------------------------- z used for periodicity checking p1 parameters 1 and 2 p2 parameters 3 and 4 p3 parameters 5 and 6 pixel screen coordinates LastSqr Modulus from the last sqr() function rand Complex random number Precedence -------------------------------------------- 1 sin(), cos(), sinh(), cosh(), cosxx(), tan(), cotan(), tanh(), cotanh(), sqr(), log(), exp(), abs(), conj(), real(), imag(), flip(), fn1(), fn2(), fn3(), fn4(), srand(), asin(), asinh(), acos(), acosh(), atan(), atanh(), sqrt(), cabs() 2 - (negation), ^ (power) 3 * (multiplication), / (division) 4 + (addition), - (subtraction) 5 = (assignment) 6 < (less than), <= (less than or equal to) > (greater than), >= (greater than or equal to) == (equal to), != (not equal to) 7 && (logical AND), || (logical OR)
Precedence may be overridden by use of parenthesis. Note the modulus squared operator |z| is also parenthetic and always sets the imaginary component to zero. This means 'c * |z - 4|' first subtracts 4 from z, calculates the modulus squared then multiplies times 'c'. Nested modulus squared operators require overriding parenthesis: c * |z + (|pixel|)|
The functions fn1(...) to fn4(...) are variable functions - when used, the user is prompted at run time (on the Z screen) to specify one of sin, cos, sinh, cosh, exp, log, sqr, etc. for each required variable function.
Most of the functions have their conventional meaning, here are a few notes on others that are not conventional. The function cosxx() duplicates a bug in the version 16 cos() function. Then abs(x+iy) = abs(x)+i*abs(y), flip(x+iy) = y+i*x, and |x+iy| = x*x+y*y.
The formulas are performed using either integer or floating point mathematics depending on the F floating point toggle. If you do not have an FPU then type MPC math is performed in lieu of traditional floating point.
The 'rand' predefined variable is changed with each iteration to a new random number with the real and imaginary components containing a value between zero and 1. Use the srand() function to initialize the random numbers to a consistent random number sequence. If a formula does not contain the srand() function, then the formula compiler will use the system time to initialize the sequence. This could cause a different fractal to be generated each time the formula is used depending on how the formula is written.
Remember that when using integer math there is a limited dynamic range, so what you think may be a fractal could really be just a limitation of the integer math range. God may work with integers, but His dynamic range is many orders of magnitude greater than our puny 32 bit mathematics! Always verify with the floating point F toggle.
The possible values for symmetry are:
XAXIS, XAXIS_NOPARM YAXIS, YAXIS_NOPARM XYAXIS, XYAXIS_NOPARM ORIGIN, ORIGIN_NOPARM PI_SYM, PI_SYM_NOPARM XAXIS_NOREAL XAXIS_NOIMAG
These will force the symmetry even if no symmetry is actually present, so try your formulas without symmetry before you use these. [END EXCERPT]
First, a disclaimer: This document is not intended to be a complete course on complex math. If you want to learn more about complex numbers, find a good algebra text. FRACTAL CREATIONS also has a good summary of the math involved here. But even if you are unfamiliar with complex numbers, read on. Don't be intimidated by the word "complex"!
Let's go over some of the fundamental concepts that you'll need to get started. Since you already have a copy of Fractint, you have undoubtedly spent some time exploring the Mandelbrot set (M-set), easily the most famous fractal. By reviewing some of the details about how this fractal is generated, you'll be better equipped to imagine new varieties of fractal formulas.
This fractal is called a "set" because it is a set of points on a two-dimensional plane, somewhat like the graphs you probably had to draw in your algebra classes. The Mandelbrot set exists in the "complex plane", so-called because it is composed of complex numbers.
You should know that a complex number has two parts: the real and the imaginary. Just as the real number system is the union of the rational and the irrational number sets, so the complex number system is a union of the real and the imaginary numbers. An imaginary number is any real number multiplied by the square root of -1. This square root of -1 has a special name: i. So a complex number which had a real component 8.5 and an imaginary component 3.2 could be written as 8.5 + 3.2i, or in parser notation as (8.5,3.2). And because the reals are a subset of the complex numbers, any real number is also a complex number; that is, 2 = 2 + 0i or (2,0).
You can perform arithmetic with complex numbers; addition, subtraction, multiplication and division are all possible, and follow the rules of basic algebra with 'i' being treated as a variable. Fractint also supports exponents (X^Y means X to the power of Y) and a variety of functions that operate on complex numbers, such as sin(), tan(), etc. I won't belabor this subject further for now, except to point out that when a complex number is operated on mathematically, both the real and the imaginary parts of the number may change; this concept is important in the discussion that follows. (A further discussion of complex arithmetic can be found in section 11.5, "Dissecting A Formula With Algebra", and functions are described in section 7.2.5, "Functions".)
In the complex plane, the horizontal axis corresponds to the real number line while the vertical axis corresponds to the imaginary number line. Any particular complex number, therefore, can be plotted as a point on the plane, and any point on the plane has a complex number that corresponds to it. The real part of the number determines the horizontal placement of the point, and the imaginary number determines the vertical. The origin of the graph (the place where the axes cross) is 0 + 0i.
As a prelude to examining formulas, let's look at the processes involved in deciding whether a particular point on the complex plane belongs to the M-set. Since the concepts involved are somewhat abstract, we'll try to create an analogy that is easier to visualize, and talk in very general terms at first.
Imagine a circle drawn on the ground, with a little ball sitting in the center. We'll pick a spot on the ground, somewhere within the circle, and call that the "test point". Now we will start moving the ball in discrete steps according to a set of specific rules (which we won't describe yet) and watch the path that the ball takes.
The first step always moves the ball over to our test point. The second step moves it to a different location, and the third step to yet another location. We'll keep applying the rules of movement, over and over, calculating a new position for the ball each time, and counting the number of moves we make.
If we try this process for several different test points, we will see something very interesting. For some test points, the ball seems to settle into a fairly predictable path, something like the orbit of an object in space -- it moves from spot to spot, but it never strays outside of the circle drawn on the ground. For other test points, the ball may move around within the circle for a while and then exit. And for some test points, the ball leaves the circle after very few moves.
Now let's try to categorize the different test points, according to the behavior of the moving ball. If the ball never leaves the circle, we'll color the test point blue, but if the ball *does* leave the circle then we'll give the test point a different color, based on the number of steps required make the ball cross the boundary. If you did this for enough test points, an image of the Mandelbrot set would appear!
Let's move beyond our analogy now and get more specific. Instead of the ground, visualize the complex plane, and instead of a ball, visualize a moving point called 'Z'. Now picture a circle on the plane, centered on the origin, with a radius of 2.
First, we'll choose a point to test; let's say 0.2 + 0.5i. Next we must define two complex variables, Z and C, such that Z = 0 + 0i and C = the value of the test point, ie C = 0.2 + 0.5i.
Then the following algorithm is iterated (repeated over and over): Calculate the value of Z^2 + C, and then place the result in Z. Since Z has a new value, find the point on the complex plane that corresponds to Z, and then check to see if the distance between Z and the origin exceeds 2. If the distance is greater than 2 (Z is outside of the circle) then the test point is *not* in the Mandelbrot set, and you may stop calculating values for Z. But if Z remains in the circle, we move back to the top of the loop and calculate a new value for Z and check it again.
In our example, if we start with Z = 0 + 0i and C = 0.2 + 0.5i, after the first time through the loop we now see that Z = 0.2 + 0.5i. Since this falls within the "bailout" circle we will calculate again, with the result that now Z = -0.01 + 0.7i. The next iteration ends with Z holding the value -0.2899 + 0.486i.
We could repeat this process over and over, noting that Z shifts its position with each iteration and yet never exits the bailout circle.
But because our time and patience have limits, we couldn't (and wouldn't want to) repeat the experiment an infinite number of times! This is where the value for maximum iterations, set on Fractint's X menu, comes in. The default value for maximum iterations is 150. This means that if the program goes through the iterated loop 150 times, and Z has never strayed outside the bailout circle, Fractint *assumes* that the test point (C) is indeed part of the set, colors it accordingly, and moves on to another test point.
Now what if the test point had the value 1.5 - 1.2i? After the first iteration, Z = 1.5 - 1.2i, which is still barely within the bailout circle. After the second iteration, Z = 2.31 - 4.8i. This time Z has strayed out of the circle, so the test point is *not* part of the M-set. Because the bailout condition has been met, Fractint stops iterating the formula and colors the test point. By default, Fractint chooses a color based on the number of iterations required to make Z exit the bailout circle, but this can be changed by various options on the Fractint menus.
I have mentioned two conditions that cause Fractint to stop looping through the formula: 1) the bailout condition is met, and 2) the maximum number of iterations has been reached. There is one other condition that can cause Fractint to stop iterating: periodicity. If Fractint detects that Z has fallen into a periodic loop, repeating the same values over and over without leaving the bailout circle, it reasonably assumes that Z will *never* exit the circle and stops iterating even though the maximum number of iterations may not have been performed yet. This is one of the reasons that Fractint is so much faster than other fractal programs you may have tried.
We're almost there, but before Fractint can create a picture of the Mandelbrot set it must settle a couple of problems, both of which have to do with infinity.
First, you should see that in the complex plane there is an infinite number of points. Obviously Fractint can't test them all. So, it chooses a subset of the points, defined by the corners of your zoom box, and only considers points within that box.
But even within that box, there is an infinity of possible points to test. Here, the resolution of your computer display is used to resolve the problem. Remember that a picture on your screen is composed of little dots called pixels. Fractint chooses points on the plane that correspond to the locations of the pixels, and only tests those points. (It can create images at a higher resolution than your display via the "disk-video" modes, but we won't go into that.) One point per pixel is enough.
So now we have a finite number of points to test. Fractint moves from pixel to pixel, finding the value on the complex plane that corresponds to each pixel and performing the test loop. Pixels are colored dark blue (by default) if they are deemed part of the M-set, and a different color (normally based on number of iterations needed to exit the bailout circle) if they are not.
We can instruct Fractint to do all of this (and more) with the following formula:
Mandelbrot (xaxis) { ;The classic Mandelbrot set z = 0, c = pixel: z = z*z + c |z| < 4 }
Perhaps the most fundamental point I could make is that a Fractint formula is actually a little computer program, not a set of mathematical equations. If you don't take this into account, you will end up very confused! For example, consider the following statement:
z = z + 1
Interpreted as an equation, that's nonsense. Instead, it is a program statement that means "Calculate the value of z + 1, and then set z to equal that value." Variables in a Fractint formula can, and often do, change values from one iteration to the next.
Let's look at some formulas and see what parts they may have. We'll start with the following formulas:
frm-A (xaxis) { ;Another formula for the Mandelbrot set z = const = pixel: z = z^2 + const |z| < 4 } frm-B { ;A generalized Julia formula ;For the traditional Julia algorithm, set FN1() to SQR, ;and then try different values for P1 z = pixel: z = fn1(z) + p1 |z| <= (4 + p2) }
It doesn't matter whether you use upper or lower case; "pixel", "PIXEL" and "Pixel" all refer to the same variable.
Z is the name you should give to the primary variable; usually this is the variable that is tested at the end of each iteration. Naming it 'Z' is not *required* but it is highly recommended, because Fractint's periodicity testing is set up to look for patterns in the values of Z.
You may wonder about the difference between Z and |Z|. Those '|' characters change the meaning completely. While Z is just the name of a variable, |Z| tells Fractint to determine the distance (the modulus) between Z and the origin; this is why it is used in the bailout test. In actuality, Fractint calculates the *square* of the distance, so the '|' characters are sometimes called the "modulus squared" operator; more on that in a moment.
Let's use a concrete example. Suppose Z has a value of (3,-4), or 3 - 4i. We can use the Pythagorean theorem to determine the distance between this point and (0,0). If you were to find this point by first drawing a horizontal line from (0,0) to (3,0), and then a vertical line from (3,0) to (3,-4), you would have two legs of a right triangle, while the line from (3,-4) to (0,0) would be the hypotenuse. Draw this out on a piece of graph paper if it isn't clear so far. Now the Pythagorean theorem states that the sum of the squares of the legs will equal the square of the hypotenuse. This means the distance from the origin to Z can be calculated as the square root of (3^2 + -4^2), which works out to 5. Obviously, this Z has strayed outside the bailout circle!
Fractint uses a small modification of the system I just described. Instead of checking to see if square_root(x^2 + y^2) < 2, Fractint checks if x^2 + y^2 < 2^2. These two expressions are mathematically equivalent. The advantage of the second way is that Fractint can avoid calculating the square root, which is much harder to do than calculating a square! This is another way that Fractint speeds up the calculation process. It also explains why the bailout tests in the Mandelbrot and frm-A formulas are written "|z| < 4" rather than "|z| < 2".
This "modulus squared" technique is a bit subtle, and can lead to some confusion if not properly understood, but it has speed benefits that few of us would want to give up!
Although P1, P2 and P3 have predefined names, the values of these variables can be chosen by the user when the formula is first selected or via the Z menu. For example, in the frm-B formula, P1 is used as a user-determined constant that is added to Z each iteration, while P2 varies the bailout condition: the radius of the bailout circle will be the square root of (4 + p2).
You can add other variables with names of your choice. If you'll compare frm-A with the Mandelbrot formula, you'll see that (among other differences) there is a variable called 'c' in Mandelbrot and a corresponding variable called 'const' in frm-A. These variables serve exactly the same purpose -- they just have different names. The variable name 'c' is traditionally used in Mandelbrot formulas, but Fractint does not require it. All of this is just to illustrate that you have the power to choose your own names for your variables. It is good practice to avoid confusion where possible; one way to help is by using descriptive variable names. But as I noted above, use the variable name 'Z' for your "main" variable whenever possible.
You can explicitly write specific functions into your formulas. For example, a formula for the Mandelbrot set might include the expression "z = sqr(z) + c".
Another option is to include user-selectable functions, as in frm-B. You may include up to four different ones, and they are designated FN1() ... FN4(). They are given specific values on the same screen as P1, P2 and P3; that is, the Z menu. So the example from the previous paragraph could also be written "z = fn1(z) + c". Of course, this would require the user to set FN1 to SQR in order to make the M-set; other functions would give different results. With Fractint version 19.0, there are 26 functions available via the user-selectable functions!
The user-selectable functions are a double-edged sword. On the one hand, they allow for much more flexibility while exploring, because a formula becomes capable of creating many different kinds of fractals. Using combinations of user-selectable functions multiplies the possibilities; a formula that uses just two of them is the equivalent of 676 different formulas with hard-coded (explicitly written) functions, while a formula that uses all four is the equivalent of 456,976 hard-coded formulas! In this way, a formula really becomes a "formula template" and helps you save space and time.
On the other hand, this sort of formula can be confusing to use, especially for the new or casual user. Someone who just wants to explore the Mandelbrot set may not appreciate being asked to know and remember that they must set FN1 to SQR in order to get what they are looking for. There may be incredible images buried within such a formula, but they require the user to do some digging to get to them!
I must confess a tendency to go hog-wild with these user-selectable functions, especially in my earlier efforts. My more recent formulas typically just use two of them. In most cases two should be plenty, I think, but of course that's entirely up to you.
Now here's a list of the user-selectable functions. Remember that we are using the complex number system here, so many functions are different from (but related to) the standard trigonometric functions that you might know about; more information about these functions can be found in the Fractint documentation. Also note that some of the comments use the word "argument" -- this is the number that is "fed" to the function.
abs() ---- Real and Imaginary Absolute Value. Returns the argument after making sure both the real and imaginary parts are positive. Abs(-3,-4) == (3,4). acos() --- Arccosine. acosh() -- Hyperbolic Arccosine. asin() --- Arcsine. asinh() -- Hyperbolic Arcsine. atan() --- Arctangent. atanh() -- Hyperbolic Arctangent. cabs() --- Complex Absolute Value. Returns the distance between the complex number and the origin. Cabs(-3,4) == 5. conj() --- Complex Conjugate. Returns the argument after reversing the numeric sign of the imaginary part. Conj(1,-3) == (1,3) and conj(1,3) == (1,-3). cos() ---- Cosine. cosh() --- Hyperbolic Cosine. cosxx() -- When the cos() function was first added to Fractint, it had a programming bug. After the bug was discovered, the corrected cos() was added, but the original function was retained under the name cosxx(), so that formulas and images made with the original function could be recreated. Cosxx() returns the same value as cos(), except that the sign of the imaginary part is reversed. Cosxx(z) == conj(cos(z)). cotan() -- Cotangent. cotanh() - Hyperbolic Cotangent. exp() ---- Exponential. flip() --- Returns the argument after swapping the values of the real and imaginary parts. Flip(1,-3) == (-3,1). ident() -- Identity. Returns the argument unchanged. Suppose a formula contains the expression "fn1(z*z) + c", but you just want to see the results of z*z + c. You can do this without rewriting the formula by setting fn1() to ident(). Ident(z) == z. log() ---- Natural Log. recip() -- Reciprocal. sin() ---- Sine. sinh() --- Hyperbolic Sine. sqr() ---- Square. sqrt() --- Square Root. tan() ---- Tangent. tanh() --- Hyperbolic Tangent. zero() --- Returns 0. This allows you to "turn off" an expression without rewriting the formula. If you were using a Mandelbrot mutation with the iterated section "z = z*z + c + fn1(z)", you could see the normal Mandelbrot set by setting fn1() to zero(). Zero(1,-3) == (0,0).
A few other functions can be hard-coded, but aren't available through the user-selectable functions.
imag() --- Returns the imaginary part of the argument as a real number. The imaginary part of the returned value is zero. Imag(1,3) == (3,0). real() --- Returns the real part of the argument. The imaginary part of the returned value is zero. Real(1,3) == (1,0). srand() -- Uses the argument to "seed" the random-number generator.
You can also "chain" assignments, as I did in frm-A. The expression "z = const = pixel" results in both 'z' and 'const' getting the value of the variable 'pixel'.
There are a couple of things you should know about the way Fractint makes comparisons between complex numbers.
First, be aware that only the *real* parts of complex numbers are compared. To Fractint, if A = (1,1000) and B = (2,1), then A < B. Please note that we are comparing the *values* of complex numbers here, not the *distance* of those numbers to the origin. So for this case A < B, but |A| > |B|.
Second, you should know that comparisons will always evaluate to either TRUE or FALSE. This may seem like a simple-minded observation, but we'll come back to it later.
Parentheses can also be used simply to make the meaning of a complicated expression clearer to you and your readers. Just remember that your parentheses *must* come in matched sets. For every '(' there must be one ')'.
frm-C1 { z = 0 c = pixel: z = sqr(z) + c |z| < 4 } frm-C2 { z = 0, c = pixel: z = sqr(z) + c, |z| < 4 }
Although functionally the same, you may prefer the "style" of one over the other. We'll return to style in section 10.0.
In some formula files, you may notice formulas in which almost every line ends with a comma or semicolon. (For examples, look at Cardioid and CGNewtonSinExp.) I have been told that an earlier version of the parser required commas or semicolons to separate the lines, but this requirement was subsequently removed. The practice continues, though, apparently spread by simple imitation. (I say this with some confidence because I did the same thing at first.) While ending a line with a comma or semicolon is not necessary, it shouldn't cause any harm either. My preference is to use them only when needed.
Now that we've looked at some of the parts we can use, let's talk about how to put them together into a working formula. In my opinion, any formula can be divided into at least three sections and at most five, with four being the most common arrangement. The three required sections are 1) the name, 2) the initialization section, and 3) the body of the iterated loop. Also present in almost all formulas is 4) a bailout test. The section most often omitted is 5) the symmetry declaration.
Our Mandelbrot formula has all five:
Name Symmetry | | V V Mandelbrot (xaxis) { z = 0, c = pixel: <-- Initialization z = z*z + c <-- Body of loop |z| < 4 <-- Bailout test }
(Be careful to only use one colon per formula. Depending on its location, an extra colon may trigger error messages or cause the formula to behave in unintended ways. At best, the redundant colon is ignored.)
In our Mandelbrot example, the following happens once per pixel: Z is set to 0 (since the imaginary part is unspecified, it is also set to 0) and C is set to the value of PIXEL. Recall that PIXEL gets its value automatically from Fractint.
If you are familiar with the DO/WHILE loop construct found in Pascal, C, and other languages, then you will understand how a parser loop works. The entire body of the loop is performed at least once, and then the parser decides whether it is appropriate to loop again (that is, move back to the colon) or to quit iterating.
Look at the bailout test for the Mandelbrot formula: |z| < 4. From our previous discussion of how the Mandelbrot algorithm works, we can see that Fractint interprets this to mean "If the modulus squared of z is less than 4, then perform the loop again." In other words, if the answer to the bailout test is *false*, then it is time to bail out of the loop.
I should point out that it is possible to write a formula that has no bailout test, but I don't recommend it. I'll come back to this subject in section 12.3, "Pathological Formulas".
Now let's try to tie all of the parts together by looking at some examples in detail. The next two formulas are taken from FRACT001.FRM, with some comments added.
Cardioid { ;author not listed z = 0, x = real(pixel), y=imag(pixel), c=x*(cos(y)+x*sin(y)): z=sqr(z)+c, |z| < 4 } CGNewtonSinExp (XAXIS) { ;by Chris Green ; Use floating point, and set P1 to some positive value. z=pixel: z1=exp(z), z2=sin(z)+z1-z, z=z-p1*z2/(cos(z)+z1), .0001 < |z2| }
These two formulas have some similarities, but their differences are especially interesting. Let's look at some of the differences, section by section.
First, notice that CGNewtonSinExp declares XAXIS symmetry, while Cardioid has no symmetry declaration. This part is always optional.
Next, compare the initialization sections. In CGNewtonSinExp, this section is very simple, but in Cardioid it is much more complicated.
Let's trace through Cardioid's "per-pixel" section. First, z is set to 0. Next, the "real" function is used. This function takes a complex number as its argument and returns the value of the real part of the number. So the real part of x is set to equal the real component of pixel. Similarly, the real part of y is set to equal the imaginary part of pixel. Finally, c gets a value based on a rather complicated looking expression that involves x and y and a pair of functions.
Now think back to the discussion of the Mandelbrot set. Remember that to complete an image, Fractint performs a set of computations (including the iterated loop) for each pixel of the image, moving from one pixel to another as the test point. This means that each time the parser executes the initialization section of Cardioid, the variable "pixel" will have a different value. This in turn means that the value of c will vary from one pixel to another.
By contrast, the initialization section of CGNewtonSinExp is utter simplicity: z gets the value of pixel.
Now look at the iterated sections of each formula, and remember that this section extends from the colon clear down to the end of the formula. Here the tables are turned. Cardioid has a very simple iterated section - just one line plus the bailout test. CGNewtonSinExp requires three lines to complete a comparatively complicated set of calculations before performing the bailout test. Four variables and three functions are involved, plus addition, subtraction, multiplication and division.
Finally, let's look at the bailout tests. Cardioid has the familiar test, "|z| < 4", which means "Stop iterating if the distance between z and the origin exceeds 2". (Re-read the passage in section 7.2.4 describing the modulus-squared operator if this isn't clear.) Broadly speaking, this sort of test says "Count how many times the formula must be iterated before z heads off in the direction of infinity." And for points that are not part of the set, z tends to do just that: the different color-bands of the standard M-set image reflect how many iterations were required before z crossed the line. Fractals based on this general algorithm are sometimes called "Escape-Time To Infinity" fractals, because membership in the set is based on whether or not z "escapes" from the bailout circle.
By contrast, look at the bailout test for CGNewtonSinExp, ".0001 < |z2|". Although the tests may look similar at first glance, there is something fundamentally different here. In this formula, the parser is instructed to keep iterating as long as |z2| *exceeds* the value .0001; that is, as long as it is *outside* the bailout circle. This kind of formula is sometimes called "Escape-Time To A Finite Attractor", and is used in the various "Newton" and "Halley" fractal types found in Fractint.
Let's recap by tracing through both formulas once more, looking for details that we may have missed the first time through.
First, when Cardioid begins, Fractint notes the location of the corners of the zoom box, so the complex values that correspond to the pixels may be found. (This is not specified by the formula. Fractint does it automatically when the parser is used.) Then for each pixel, the initialization section is performed just once; remember that this section extends from the opening brace down to the colon. While this section of Cardioid is comparatively complicated, the fact that it is performed just once per pixel means it won't have a big impact on the speed of the formula. Then, the iterated section is repeated over and over until 1) z escapes the bailout circle, 2) maximum number of iterations is reached, or 3) periodicity in the orbit of z is detected. In each iteration, a new value for z is found using the *current* value of z as one of the terms of the calculation, and this new value is then assigned to z.
At the beginning of CGNewtonSinExp, Fractint is told to use the "xaxis" symmetry-drawing technique. After the location of the zoom-box corners has been noted, the parser then reads the initialization section one time per pixel. For this formula, that section is extremely simple. Now the parser moves to the iterated section. It performs all of the specified calculations that follow the colon, and then performs the bailout test. If the test evaluates to TRUE, and the other bailout conditions are not met, the parser loops back to the colon and starts calculating again.
Since the bulk of the calculating takes place within the iterated loop, an iteration of CGNewtonSinExp will take longer than an iteration of Cardioid. I don't see how that could be avoided in this case, but as I'll show you later, you can often speed up a formula by putting as much calculating as possible in the initialization section rather than in the iterated loop.
At this point, we've covered the essentials you'll need to begin writing formulas. You now know the elements most commonly used in formulas, and you know some basic rules that govern how those elements are combined.
But even though you have been shown the parts, we haven't really discussed how to go about actually writing a formula. Just what is the process?
As you would probably guess, there are many different approaches available. The approach you choose will depend on your temperament and your mathematical abilities. The following list is certainly not exhaustive, but it may give you some ideas on how *you* might get started.
Some people have so deep an understanding of the mathematics of fractals that they can use their insights to discover new fractals. Benoit Mandelbrot, for instance, understood the mathematics of Julia sets well enough to envision a new fractal that would serve as a "catalog" of all Julias. This new fractal is, of course, the Mandelbrot set. Few of us have this sort of deep insight, unfortunately.
Some beautiful fractals were found by investigating mathematical procedures developed for other purposes. For instance, in Fractint there are several built-in fractals and formulas for "Newton" fractals. These are based on a mathematical algorithm invented by Sir Isaac Newton for finding the roots of numbers. Surely he didn't have fractals in mind when he invented his method, but there they are! The "Halley" types are based on adaptations of another method for finding roots.
If you know of an interesting algorithm, you might want to try adapting it to the formula format to see if there are any fractals lurking within. Later, in the discussion of "Using Values From Other Iterations", I'll give another example.
This is probably the easiest way to get started and get good results quickly. Look at the following formula:
Mutantbrot { ;A mutation of the classic Mandelbrot set z = 0, c = pixel: ;standard initialization section z = z*z + c + sin(z) ;mutated iterated section |z| < 4 ;standard bailout test }
All I did was to take the classic Mandelbrot formula and add a new term to the iterated section: "+ sin(z)". I didn't have any particular insight that led me to do this, I just tried it to see what would happen. The result is certainly different from the M-set, but interesting.
One very good way to mutate a formula is to replace hard-coded functions with user-selectable functions. This is called "generalizing" the formula. For example, if a formula uses the expression "c = sqr(pixel)", you could change it to read "c = fn1(pixel)", and then experiment with different functions. Remember that just one generalized function will save you from the chore of typing dozens of hard-coded varients!
You can also try enclosing key terms within functions. In a Mandelbrot formula, you could replace "z = z*z + c" with "z = fn1(z*z) + c". Now you can see the normal Mandelbrot by setting fn1() to IDENT, but you can also get interesting results with other functions.
More examples of formulas created with the "mutation" approach can be found in my file FUBAR.FRM.
This is a reference to the old claim that if you put an infinite number of monkeys in front of typewriters and let them pound on the keys for an infinite length of time, eventually one would produce one of Shakespeare's sonnets. In this context, it means "just load your text editor and start typing". You know basically what a formula should look like, and you've seen lots of different "parts" used in other formulas, so play mad scientist. (Dr. Fractalstein?) Graft together different parts, add new ones of your own invention, and then see what happens. This approach is suitable for those of us (myself included) who are unencumbered by real math skills or insights but who want to play anyway! It often takes a lot of patience to find something interesting, but when you do it can be very exciting.
It's hard to describe the experience of writing a more-or-less random formula and seeing incredible order and complexity in the emerging image. I find it both exhilarating and spooky.
If you have a collection of formula files, look them over with your text editor. You may notice that different authors can give their formulas very different appearances. Often, these visual differences are simply differences in "style". (The formulas in FRACTINT.FRM have been edited to have a consistent style, but other formulas vary.)
Some authors like to put as much as possible on a single line, using commas to separate the different expressions, while other authors prefer to use a single expression per line. Some formulas have explanatory comments, while other formulas have none. Some authors use spaces between variables and operators, while other authors run them together. And some authors use indentation to make the different sections of their formulas more easily identifiable, while other authors do not.
These differences demonstrate the personal preferences of the individual authors; the style that you use is entirely up to you. However, I have a few biases of my own that I'd like to inflict upon you.
First, pick a style that makes sense to you, and then try to be consistent with it. This makes life easier for someone who is reading your formulas and trying to make sense of them. If you don't care about the comfort of others, remember that this unfortunate reader may be you, later on!
Second, strive for clarity. This can be achieved in different ways: adding comments, using spaces wisely, indenting, choosing informative variable names, etc. Often the addition of parentheses can help make the logical grouping of formula elements more visible, even when the parentheses are not strictly necessary.
Okay, you know the basics. You've written some formulas of your own, and the rules about formula structure are becoming second nature. Now let's talk about techniques you can use to improve the performance of your formulas, or to make them do fancy new tricks.
Warning: If you haven't already written several working formulas of your own, and aren't comfortable with the preceding information, you might want to come back to the rest of this document later. From this point on, we are venturing into material that is more complicated and subtle.
Here are some ways to make your formulas run a little faster.
speed-A { ;Demonstrates potential for speed-up z = 0: z = z*z + sin(pixel) |z| < 4 } speed-B { ;variation of speed-A showing one speed-up technique z = 0, sinp = sin(pixel): z = z*z + sinp |z| < 4 }
Both formulas will result in exactly the same image being drawn, but speed-B is much faster. Why? In speed-A, the value of sin(pixel) must be calculated *once per iteration* while in speed-B it is only calculated *once per pixel*. We can do this because the value of pixel doesn't change during the iterated section. The value of z keeps changing from one iteration to the next, however, so we can't use the same trick with that variable. On my home computer (and at my preferred resolution) I can generate the speed-A fractal in about 82 seconds, while speed-B takes only 38 seconds. Speed-A may seem a little more straightforward, but your speed-hungry users will usually prefer using speed-B!
Here's an summary of what I've learned about including conditional logic in a formula, and some warnings about possible pitfalls.
Let's look at an example. Suppose you want to include the following logic in your formula: if A is negative then it gets the value of X, otherwise it gets the value of Y. In the C language, you could write this as:
if (a < 0) a = x; else a = y;You could simulate this in a Fractint formula like this:
neg = x * (a < 0) pos = y * (a >= 0) a = neg + posor:
a = (x * (a < 0)) + (y * (a >= 0))
Now both of those parser versions are quite a bit more obscure than the C version, and C has a reputation as an obscure language! But let's examine the first parser version and see how it works.
It's time to return to the observation that a comparison will always evaluate to either TRUE or FALSE. That seems glaringly obvious on one level, but it's the details under the surface that make this technique work. The key detail is that Fractint appears to represent TRUE with a one, and FALSE with a zero.
So suppose that a = (1.5,2) and let's see what happens with an expression like "neg = x * (a < 0)". Since this is an assignment statement, the parser will first have to evaluate what is on the right side of the '=', so it will know what to put into neg. When the parser comes to the comparison expression, it will give the answer FALSE, because the real part of a is 1.5. Since the parser represents FALSE with a zero, the expression simplifies to "x * 0", therefore neg = 0.
Using similar logic, you should be able to prove that pos will end up equalling y.
Since any single comparison is either TRUE or FALSE but not both, and because of the way I set up those comparisons, you should see that when a is negative, a = x + 0, and that when it is zero or positive, a = 0 + y.
The second parser version is simply a condensation of the above, without the intermediate variables neg and pos.
Which parser version do you prefer?
PITFALL 1: The order of the expressions can make a difference.
The order in which expressions are evaluated can be important in *any* part of a formula. See section 7.2.10, for instance, for a discussion of how the rules of precedence affect the results of computations. But in the context of conditional logic, this principle can take on extra subtlety.
Chuck Ebbert is the fellow who wrote the fast new version of the parser that was introduced with version 18, so we may regard him as a real authority. In his formula file BUILTN.FRM, Chuck suggested putting the comparison *after* the multiply when using the IF..THEN trick. He advised doing it for speed gains, but it can also affect the images that some formulas produce.
Here's one such situation. Suppose you want to use the following algorithm in your iterated section: If 'z' (or more precisely, the *real* part of 'z') is negative, then z = fn1(z) + c ; otherwise, z = fn2(z) + c.
Don't be tempted to set things up like this:
IfThen-A1 { ;Demonstrates that the order of expressions can make a ;difference. In this example, the assignment is performed ;BEFORE the comparison. z = c = pixel: (z < 0) * (z = fn1(z) + c) (0 <= z) * (z = fn2(z) + c) |z| < 4 }
The comparison expressions precede the assignment expressions as you read from left to right, but it appears that the parser actually evaluates the right-hand expression (the assignment) first. You can confirm this by looking at the images produced by this formula:
IfThen-A2 { ;Functional equivalent of IfThen-A1 z = c = pixel: z = fn1(z) + c z = fn2(z) + c |z| < 4 }
Since both formulas produce the same images, I conclude that the comparisons are, in effect, being ignored.
If you wish, you can further simplify the formula:
IfThen-A3 { ;Another equivalent of IfThen-A1 z = c = pixel: z = fn2(fn1(z) + c) + c |z| < 4 }
While the preceding three formulas were instructive, they didn't do what we set out to do. Let's try again:
IfThen-B1 { ;In this formula, the comparison is performed BEFORE the ;assignment, but there's still a subtle flaw. z = c = pixel: (z = fn1(z) + c) * (z < 0) (z = fn2(z) + c) * (0 <= z) |z| < 4 }
This formula reverses the order of the comparison and assignment expressions. If you'll compare its images to those of IfThen-A1, you'll see that rearranging the expressions also changes the images.
PITFALL 2: Don't try to embed an assignment statement within a larger expression.
Okay, we've reordered the expressions in IfThen-B1 so the comparison is evaluated first. Because the assignment statements are within parentheses, it might seem reasonable to assume that an assignment will only occur if the comparison is TRUE. Certainly, that is our intent. Unfortunately, it isn't so. Look:
IfThen-B2 { ;Functional equivalent of IfThen-B1 z = c = pixel: z = (fn1(z) + c) * (z < 0) ;line A z = (fn2(z) + c) * (0 <= z) ;line B |z| < 4 }
Since IfThen-B2 produces the same images as IfThen-B1, we may assume that they are functionally equivalent. But in IfThen-B2 it is clear that *some* assignment always takes place, whether the comparison is TRUE or FALSE. This may well produce interesting results, but it isn't what we wanted. Remember that we wanted 'z' to get 'c' plus EITHER fn1(z) OR fn2(z). Putting the assignment within the parentheses didn't help us achieve conditional execution.
In the Fractint parser language (as I understand it) there's no good reason to put an assignment statement *within* a larger expression, as we did in IfThen-A1 and IfThen-B1. That sort of thing may be useful in C and other languages, but in a Fractint formula it is likely to mislead you and your readers as to what is really happening.
PITFALL 3: If you are trying to create an EITHER/OR choice, construct your formula carefully to ensure that the choices are mutually exclusive.
Let's walk through an example for IfThen-B2. Suppose z < 0; then when line A is performed 'z' will get fn1(z) + c. Depending on the function selected for fn1() and the value of 'c', this *new* value of 'z' could be either negative, positive or zero, and it will determine whether the comparison in line B is TRUE or FALSE. In fact, for any particular iteration, the comparisons in lines A and B could be TRUE and TRUE, TRUE and FALSE, or FALSE and TRUE. This is getting very complicated!
Comparisons line A line B Equivalent Expression --------------------------------------- TRUE / TRUE z = fn2(fn1(z) + c) + c TRUE / FALSE z = 0 FALSE / TRUE z = fn2(0) + c
(Why no "FALSE / FALSE"? If line A is FALSE, then 'z' gets zero. Because of the way we wrote the comparison in line B, this would make line B necessarily TRUE...)
Let's recap: In IfThen-A1 each comparison was ignored because the assignment had already been made before the comparison occurred. In IfThen-B1 the comparisons are not ignored, but our flawed logic causes the value of 'z' to change between the first comparison and the second.
Got a headache yet? Try these on for size:
IfThen-C1 { ;What we REALLY had in mind. z = c = pixel: neg = fn1(z) * (z < 0) pos = fn2(z) * (0 <= z) z = neg + pos + c |z| < 4 } IfThen-C2 { ;An alternate version of IfThen-C1 z = c = pixel: z = (fn1(z) * (z < 0)) + (fn2(z) * (0 <= z)) + c |z| < 4 }
Here we finally have the algorithm we intended to implement. We made sure that the comparisons are evaluated before the assignments. We also took care to make the comparisons independent of each other.
The point of the whole ugly exercise is this: unless you are careful, you can write a formula that operates in ways that you didn't intend.
Before I leave this subject, let me address a possible objection. It may appear that if you successfully avoid pitfall #2 (embedding an assignment within a larger expression) then pitfall #1 (order of expressions) is a non-issue. I agree that in *most* cases it will not make a visible difference. IfThen-C1 appears to give the same images whether we write "neg = fn1(z) * (z < 0)" or "neg = (z < 0) * fn1(z)", for example. But putting the comparison after the multiply does seem to speed up many formulas, and as you'll see when you read section 12.4, "A Ghost Story", there *are* rare occasions when the order of the sub-expressions appears to affect the image, even though this may seem illogical. You are free to do as you like, of course, but I plan to follow Chuck's advice until I see a good reason to ignore it.
Suppose that you have written a formula, and now you need to provide a bailout test. Even if you are just creating a normal escape-time formula, there are at least three different approaches to choose from. These different approaches are illustrated by the following three formulas. Each of these techniques has advantages, and I have used them all at various times.
bailout-A { ;Hard coded bailout value ;p1 = parameter (default 0,0) z = pixel, c = fn1(pixel): z = fn2(z*z) + c + p1 |z| < 4 } bailout-B { ;Variable default -- additive ;p1 = parameter (default 0,0) ;p2 = bailout adjustment value (default 0,0) test = (4 + p2) z = pixel, c = fn1(pixel): z = fn2(z*z) + c + p1 |z| < test } bailout-C { ;Variable default -- conditional logic ;This formula requires floating-point ;p1 = parameter (default 0,0) ;p2 = bailout (default 4,0) ;The following line sets test = 4 if real(p2) = 0, else test = p2 test = (4 * (p2 <= 0)) + (p2 * (0 < p2)) z = pixel, c = fn1(pixel): z = fn2(z*z) + c + p1 |z| < test }
Before we discuss the different approaches, examine the formula for a moment. It is a hybrid of a Mandelbrot and a Julia formula, with a couple of user-selectable functions thrown in for fun. P1 is used as a constant to be added with each iteration, and in the second and third formulas P2 is used to determine the bailout value.
Now let's look at the bailout test in "bailout-A". Hard coding a value, as in "|z| < 4", is easiest to code and easiest to understand. It also will run the fastest. But it doesn't allow the user to change the value without editing the formula. Since I have added variable functions and a user parameter (p1), this value might not be the best choice for all situations. (If the user wants to use the "biomorph" option, a variable bailout test will also be desirable because this option generally works best with a high bailout value.)
In "bailout-B", I have addressed this potential problem. Here, the value the user gives to P2 is added to 4 and stored in a variable called test. This variable is then referred to in the bailout test. If you decide that 3 would be a better bailout value than 4, you can give the real part of P2 the value -1, for example. As I noted in the section on speed-up techniques, putting the calculation of "4 + p2" in the initialization section will make the formula go faster than if the bailout test was written as "|z| < (4 + p2)".
I didn't just set the bailout equal to p2, however, because then if the user left p2 at (0,0) the resulting image would be a blank screen. It is likely that a beginner or casual user of your formula would do just that, and would probably decide that your formula is defective! If you need to allow the user to vary some value, it is best (in my opinion) to be sure that the default values produce *some* image.
This technique is still quite easy to implement. It has the drawback of being more obscure than hard coding, however. If you want the bailout value to be 16, for instance, you must understand the formula well enough to know that p2 must equal 12.
The third technique is illustrated by "bailout-C". I first saw this trick used in Chuck Ebbert's formula file that I referred to earlier. In this approach, conditional logic is used. If the real part of p2 is left at zero (or is negative) then "test" is given the value 4. Otherwise, "test" gets the value of p2.
In practice, this approach works more intuitively. If you want the bailout test to be 16, you set p2 to 16. There is a price to be paid for this intuitive operation, however: it's a little harder to write, and since it makes the formula more complicated, the formula will be a tiny bit slower.
In section 9.2 I talked about adapting an existing algorithm to the formula format. Here's an example of what I meant.
Do you recognize the following series of numbers? What should the next number be?
1, 1, 2, 3, 5, 8, 13, 21 ...
This is called the Fibonacci series. Each new number in the series is the sum of the previous two, so the next number should be 13 + 21, or 34. Let's see if we can make a formula out of this series.
We might begin with the observation that in the Mandelbrot formula each new generation of 'z' is based on the previous generation. Maybe we can adapt the Mandelbrot formula for our purpose, but we want to involve the previous *two* generations. To do this, we'll need to be able to store old values for 'z'.
Here's one way to do it:
fibo-A { ;Derived from the Fibonacci series z = oldz = c = pixel: temp = z z = z * oldz + c oldz = temp |z| < 4 }
Let's step through it. In the following discussion, I'm going to use the notation z(n) to refer to the value of z at the *end* of iteration number n, so z(3) would be the value of z at the end of the third iteration.
First we initialize our variables including a new one, 'oldz', to the value of 'pixel'. Since we haven't completed an iteration yet, let's say z is at generation zero.
Then the first line of our iterated section introduces another new variable, 'temp', which gets the value of z(0). Next we calculate a value for z(1): z(0) * oldz + c. Then oldz gets the value of temp, which is z(0).
When we started into the iterated section, z, oldz and c all had the same value. Now z almost certainly has a different value, while oldz and c still have the value 'pixel'.
Let's assume that the bailout conditions have not been met, and loop back for another iteration. Temp gets the value of z(1). Then we calculate z(2), and oldz gets the value of z(1) from temp.
Now until we stop iterating:
z(n) = z(n-1) * z(n-2) + c
What have we got here? It's not quite the same as the Mandelbrot algorithm, and it's not quite the same as the Fibonacci algorithm either. Instead, we have a hybrid of the two.
The formula makes an interesting image, but doesn't allow the user any room to play, so let's add a variable function:
fibo-B { z = oldz = c = pixel: temp = z z = fn1(z * oldz) + c oldz = temp |z| < 4 }
Now you can reproduce the fractal created by fibo-A by setting FN1 to IDENT, but you can also play with other functions.
Keeping track of old values can be useful in other ways too. Look at the following formula, an example of the "in-and-out" formulas found in my file INANDOUT.FRM:
inandout01 { ;Bradley Beacham [74223,2745] ;p1 = Parameter (default 0), real(p2) = Bailout (default 4) ;The next line sets test=4 if real(p2)<=0, else test=real(p2) test = (4 * (real(p2)<=0) + real(p2) * (0<p2)) z = oldz = pixel, c1 = fn1(pixel), c2 = fn2(pixel): a = (|z| <= |oldz|) * (c1) ;IN b = (|oldz| < |z|) * (c2) ;OUT oldz = z z = fn3(z*z) + a + b + p1 |z| <= test }
Here's the basic idea behind the "in-and-out" algorithm: Did the last iteration move z closer to the origin (0,0) or farther away from it? If closer, use one value. If farther away, use a different value. Picture the complex plane with the bailout circle centered on the origin. In my imagination this resembled a radar screen, with z moving about within the circle. Is z getting closer to the center, or farther away?
To answer that question, I kept track of the value of z from the previous iteration by storing it in a variable called oldz, and then compared the values of |z| and |oldz|.
If you feel bewildered, hold on. Let's look at the formula in detail, step by step, to illustrate how it works. To make it a little easier, I annotated the formula with additional comments, and put spaces between different functional parts to make them easier to see.
inandout01 { ;Bradley Beacham [74223,2745] ;p1 = Parameter (default 0), real(p2) = Bailout (default 4) ;The next line sets test=4 if real(p2)<=0, else test=real(p2) test = (4 * (real(p2)<=0) + real(p2) * (0<p2)) ;initialize other variables z = oldz = pixel, c1 = fn1(pixel), c2 = fn2(pixel): ;did previous iteration move z in or out? a = (|z| <= |oldz|) * (c1) ;IN b = (|oldz| < |z|) * (c2) ;OUT ;save value of z before changing it oldz = z ;calculate new z z = fn3(z*z) + a + b + p1 ;bailout test |z| <= test }
First comes the formula name. Next is a comment that tells you how the user variables P1 and P2 are used. Then we give the variable 'test' a value -- this will be used in the bailout test -- and we set up some other variables. I call my main variable 'z', and set it equal to pixel. I also create a variable called 'oldz' and set it to the value of pixel. Then I create two other variables called 'c1' and 'c2'. Recall that the general algorithm is to use one value if z moves closer to the origin, and another value if z moves away. To accomplish this, I opted to use two different versions of c: c1 gets fn1(pixel) and c2 gets fn2(pixel).
Since we just found the colon, that's the end of the initialization section. Let's proceed to the iterated section. Remember that the main question is "did z move closer to the origin, or farther away?" To answer this question, we compare |z| to |oldz|. We have to account for the possibility that they are the same, so the first comparison is stated as "|z| <= |oldz|". On the first iteration 'oldz' was initialized to equal 'z' so this will be the case, but on subsequent iterations, who knows? At any rate, if this comparison is TRUE, the statement has the value of 1, so 'a' gets the value of 'c1'; if it is FALSE, 'a' gets the value 0.
A similar comparison covers the opposite possibility. If |oldz| is less than |z|, then 'b' gets the value 'c2'; otherwise it gets 0.
Note that we made sure the possibilities are mutually exclusive. In every case, if one comparison is TRUE then the other is FALSE, but the TRUE comparison may change from iteration to iteration.
Then, we save the value of 'z' into 'oldz', just before we calculate a new value for 'z'. After the first iteration it is likely that 'oldz' and 'z' will have different values, but 'oldz' will always have the value of 'z' from the previous iteration, as in the "fibo" formulas.
To calculate the new 'z', we use "z = fn3(z*z) + a + b + p1". Because of the way we set up the comparisons, this will work out to mean either "z = fn3(z*z) + c1 + 0 + p1" or "z = fn3(z*z) + 0 + c2 + p1".
(Speaking of the comparisons, you have probably noticed that I didn't follow my own advice about writing the comparison after the multiply. When I wrote that formula, I was unaware of the possible complications involved. I'm still learning...)
Finally, we come to the relatively simple bailout test.
Whew, that was a bit of a workout! So what's the cosmic significance of it all? Beats me! If it does nothing else, though, it should show you that it is possible to invent fairly elaborate algorithms for your formulas without any notions about their ultimate mathematical significance. And if you're smart enough to see the significance of things like this, then you're in a great position to exploit that knowledge.
It should also demonstrate that saving values from past iterations can be a useful technique!
Fractint was designed to work with complex numbers. It has all kinds of mathematical operators and functions available that were written with complex math in mind.
But the fact that complex arithmetic works like algebra can be exploited for insight and for interesting results. Let's take a closer look at how complex multiplication is performed.
Remember that a complex number can be expressed as the combination of a real and an imaginary part. The complex number x + yi, for example, has a real part of x and an imaginary part of y, while 'i' represents the square root of -1.
To find the square of the number, we can treat the complex number as an ordinary algebra expression, with 'i' being a variable. This means that (x + yi) * (x + yi) == (x^2 + 2xyi + yi^2). Now if you remember that i^2 == -1, this can be simplified to ((x^2 - y^2) + 2xyi).
We can write a formula that recreates this process, doing complex arithmetic the hard way. For want of a better term, I call this "dissecting" the formula.
dissected-A { ;A dissected Mandelbrot z = 0, c = pixel: x = real(z), y = imag(z) ;isolate real and imaginary parts newx = x*x - y*y ;calculate real part of z*z newy = 2*x*y ;calculate imag part of z*z z = newx + flip(newy) + c ;reassemble z |z| < 4 }
In this formula, I isolate the real and imaginary parts of z, and then perform the math described above. Let's suppose for a moment that z = 2 + 3i, and step through the iterated section of the formula.
First, the real part of z is extracted by using the "real" function. This result is put in a complex variable called 'x'. Then the imaginary part of z is extracted using the "imag" function, and this is put into 'y'. Thus, x = (2,0) and y = (3,0).
Now, values for the real and imaginary parts of z*z are calculated by emulating the algebraic analysis above. That is, we find the value of x*x - y*y; that's the real part of z*z. Then we find the value of 2*x*y, which is the imaginary part of z*z.
Now we "reassemble" z using these values. Remember that when we used the "imag" function and put the result in 'y', it went into the *real* part of 'y'. This means that we have to convert the value in 'newy' into an imaginary number. We do this with the "flip" function. The "flip" function, in effect, turns a complex number upside down by swapping the real and the imaginary values. Thus, flip(x + yi) returns y + xi. If x = (2,0) and y = (3,0), then newy = (12,0). Therefore, flip(newy) = (0,12). Finally we add c and perform the bailout test.
If you'll run this formula, you'll see that it does indeed produce the Mandelbrot set. But that's a lot more work than the standard Mandelbrot formula. Why bother?
The answer is that by "laying the parts out", so to speak, you can perform operations on them that would be difficult to do with a formula written in the normal way. Consider the next formula:
dissected-B { ;A mutation of "dissected-A" z = 0, c = pixel, k = 2 + p1: x = real(z), y = imag(z) newx = fn1(x*x) - fn2(y*y) newy = k*fn3(x*y) z = newx + flip(newy) + c |z| < 4 }
By experimenting with various values for fn1(), fn2(), fn3() and p1, you can produce all sorts of interesting variants on the Mandelbrot set. I suggest starting with all of the functions set at "ident" and p1 at 0, and then changing one of them at a time.
Practice your complex arithmetic so it becomes easier to do, and you'll find that it is possible to dissect many different formulas. See my OVERKILL.FRM for some other examples.
Fractint has built-in mechanisms to count the iterations of our formulas. Normally we just rely on the automatic counter, but sometimes it's more fun to be abnormal. Here's a simple example of how you can manually track the iteration count, to produce fractals that are definitely out of the ordinary.
shifter { ;Use a counter to shift algorithms z = c = pixel, iter = 1, shift = p1, test = 4 + p2: lo = (z*z) * (iter <= shift) hi = (z*z*z) * (shift < iter) iter = iter + 1 z = lo + hi + c |z| < test }
Let's first look at the mechanics of the formula. Note that there is a variable called 'iter', which is used to keep a running count of the iterations. For each pixel it is initialized with 1, and in each pass through the loop the value is incremented; so at the beginning of the first iteration 'iter' will equal 1, on the second iteration it will equal 2, etc.
Also note that the formula uses conditional logic to select between two algorithms: do we calculate z*z or z*z*z? The question is settled by comparing the iteration number to a variable called 'shift', which equals p1. (I could have just used p1 in the formula, but I think using a variable called 'shift' makes the meaning a little clearer.)
Suppose you set p1 to 75, and left the value for maximum iterations at 150. This means that for the first 75 iterations, the formula would calculate z = z*z + c, but from iteration 76 to 150, it would calculate z = z*z*z + c. In other words, it is shifting mid-stream from the normal Mandelbrot algorithm to the "Cubic Mandelbrot" algorithm.
Set p1 to equal 0, and you'll see a Cubic Mandelbrot. Set it at 150 and you'll see the normal Mandelbrot. But when you use a value in-between, you'll get something much stranger. Usually it looks like a normal Mandelbrot with strange growths inside its lake area. Weird! (Note: this formula seems to work best with Fractint's periodicity logic turned off. To do this, just hit the G key, and then enter "periodicity=0".)
Now reflect that this is a very simple example. Can you invent more elaborate and interesting formulas that use the iteration counter idea?
We've already looked at some of the possible pitfalls when you use the conditional logic (if..then) technique. Now let's look at a few other tricky areas.
Fractint's symmetry-drawing techniques can be a big time saver if used correctly. If you *know* that the fractals produced by a formula will always be symmetrical, then it is smart to declare the symmetry. But what if you are wrong? For better or worse, Fractint will attempt to follow your instructions anyway. Let's look at an example.
sym-A { ;Non-symmetrical fractal z = c = pixel, k = (2.5,0.5): z = z^k + c |z| < 4 } sym-B (xaxis) { ;Sym-A with symmetry declared in error z = c = pixel, k = (2.5,0.5): z = z^k + c |z| < 4 }To see what I'm talking about, first load the sym-A formula and look at the image it makes. Clearly, the fractal is asymmetrical. Now look at the image produced by sym-B.
The only difference between the formulas is the symmetry declaration, which doesn't really change the nature of the mathematical object described by the formula. We told Fractint to take a shortcut when drawing the image, so it did. You may even prefer the symmetrical image, but the truth remains that it is just an illusion. If you will rotate the zoom box slightly on sym-B, Fractint will stop trying to use the symmetry-drawing techniques, and the real fractal will come out.
I pointed out earlier that the formula CGNewtonSinExp specifies xaxis symmetry. This is unfortunate, in my opinion, because if the imaginary part of p1 is anything other than zero, that fractal appears to lose its symmetry. Again, this can be demonstrated by rotating the zoom box.
My point is this: Please be *sure* that your formula produces only symmetrical images before including a symmetry declaration. Otherwise you are likely to provoke confusion in your less experienced users, and to trigger misguided complaints about "Fractint bugs".
Luckily, there are some ways to test for symmetry problems. First, make sure that the formula you are testing *does not* have a symmetry declaration. Now load the formula into Fractint and try this:
If you do these things and *still* only get symmetrical images, you are probably home free. But be sure you try a lot of different parameters before drawing your conclusions.
The formula parser will tell you about some kinds of errors when it finds them. If you try to use the expression "z = (z*z + (c*z)", it will recognize that there is a mismatch in the number of parentheses, for example.
A different type of error goes unreported, though. Look at the following formulas:
frm-D1 { ;Unparsable expression ignored z = c = pixel: z = z*z + sin z + c |z| < 4 } frm-D2 { ;fixed version of frm-D1 z = c = pixel: z = z*z + sin(z) + c |z| < 4 }
In frm-D1 there is an incorrectly written expression, "sin z". Although the intent may be clear to you or me, the parser can't make proper sense of it. (Computers tend to take things *very* literally.) Since the formula produces a normal-looking Mandelbrot set, it appears to me that the parser is simply skipping over this part of the formula.
Frm-D2 is here to show you what the fractal would look like if frm-D1 had been written correctly.
Some formula writers ignore (or are unaware of) the conventions, and produce formulas that "work" for the wrong reasons.
For instance, take another look at IfThen-A1. This formula could be written with the best of intentions, and it may even produce an interesting fractal, but it is based on a misunderstanding. If you were the author of IfThen-A1, and if you understood the previous discussion about this formula, then I believe you should either simplify it as in IfThen-A2 or IfThen-A3, or correct it as in IfThen-C1 or IfThen-C2.
In general, I think we should try to write formulas that someone else could comprehend. Even well-written formulas can be hard to understand, so let's not carelessly (or deliberately) make it worse! IfThen-A1 is obscure at best, and is likely to mislead the unwary reader.
Even worse, some formula authors leave out parts. Consider:
weirdo { ;Mandelbrot with no bailout test z = c = pixel: z = z*z + c }
This one has an initializing section and an iterated section, but there's no bailout test. So what stops it from iterating? In my first analysis, I assumed it would stop iterating only when 1) the maximum number for iterations had been reached, or 2) 'z' fell into a periodic loop. Recall that in both of these situations, Fractint assumes that the "test point" (the value of pixel) is part of the set; therefore I expected that the whole screen would be colored blue.
If you run the formula, though, you will see that I was wrong. I get what looks like a normal Mandelbrot lake, but the color bands outside the set are different. (Warning: if you're using Fractint version 18.2 or earlier and don't have an FPU or math coprocessor, the program might crash.)
Luckily for me, Tim Wegner was able to explain what was happening here. In brief:
The parser uses a stack to store values. (If you don't know what a stack is, just think of it for now as a location in memory.) After the iterated section has been performed, the value remaining on the stack determines whether the formula will be reiterated. It is just *assumed* that the last operation will be a comparison.
Now if the value left on the stack is zero (as it would be with a FALSE comparison), then the iterations end. Otherwise (assuming we haven't reached the maximum iteration number or fallen into a periodic loop) the parser loops back up to the colon.
In "weirdo", the value left on the stack would be the value assigned to 'z'. So if by chance real(z) == 0, the iterations would stop and the parser would choose the next test-point. For most test-points outside the M-set, however, the formula will keep iterating until 'z' becomes so large that a math error occurs; after this happens (and assuming Fractint didn't crash) the value left on the stack is zero. Fortunately, versions 19.0 and higher should be able to take these errors in stride and move on to the next test point, even without an FPU or coprocessor.
As Tim put it, instead of using an escape-time algorithm, this formula could be said to use a "crash-and-burn-time algorithm".
A formula that depends on math errors to produce an image! And "weirdo" is not unique; several formula files have been circulated containing formulas that lack bailout tests.
I believe that this is undesirable, because even when it "works" the *way* that it works is exceedingly obscure. If this fails to persuade you, consider that when you create images that depend on computational quirks, math errors or bugs, you may find it impossible to reproduce your images later on when the software has been revised.
On the other hand, Tim has pointed out that most real-life fractals are poorly understood (if at all), and that many beautiful and mysterious images have been produced that depend on computational quirks or inaccuracies. Why discount these images, just because we don't fully understand how they came about?
What do you think?
When formulas get complicated, it can be very difficult to understand what's happening. That comment just proves my keen grasp of the obvious, I suppose, but here's an interesting example I found recently.
(Warning -- murky water ahead!)
ghost { ;Demonstrates strange parser behavior ;To see effect, use floating point and make sure ;FN2() is not IDENT z = oldz = c1 = pixel, c2 = fn1(pixel) tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): oldx = real(oldz) - rt oldy = imag(oldz) - it olddist = (oldx * oldx) + (oldy * oldy) x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = (dist <= olddist) * (c1) b = (olddist < dist) * (c2) oldz = z z = z*z + a + b |z| <= 4 } ghostless-A { ;One solution to the ghost problem -- reorder expressions z = oldz = c1 = pixel, c2 = fn1(pixel) tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): oldx = real(oldz) - rt oldy = imag(oldz) - it olddist = (oldx * oldx) + (oldy * oldy) x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = (c1) * (dist <= olddist) ;Reverse order of value and comparison b = (c2) * (olddist < dist) ;Ditto oldz = z z = z*z + a + b |z| <= 4 } ghostless-B { ;Another solution to the ghost problem -- reinitialize z = oldz = c1 = pixel, c2 = fn1(pixel) tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): oldx = real(oldz) - rt oldy = imag(oldz) - it olddist = (oldx * oldx) + (oldy * oldy) x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = b = 0 ;Make sure a & b are set to zero a = (dist <= olddist) * (c1) b = (olddist < dist) * (c2) oldz = z z = z*z + a + b |z| <= 4 }
First, look at the "ghost" formula. I created this formula while experimenting with new variations on the "in and out" theme. You won't need to try to follow all of its logic; just note that this is a fairly complicated formula, with several variables and functions, an iterated section with many steps, and a couple of lines that use the conditional logic technique.
If you'll look at the images that the "ghost" formula makes (make sure you're using floating-point math) you should notice a very strange thing: it appears that there is more than one image, and that these images are somehow superimposed over each other.
This is an interesting effect, but it is due to the parser behaving in a way that I hadn't intended. After quite a bit of experimenting, I found a few ways to make the ghosting effect go away.
For example, set FN2() to IDENT. No more ghosting; any other function will make it reappear, however.
Now try the same formula with integer math. Again, no ghosting problem, regardless of the function selected for FN2().
Turn floating point back on and look at the images made by "ghostless-A" and "ghostless-B". Once again, the ghosting effect vanishes. If you'll compare these formulas to "ghost", you should see that the only difference is in the way the conditional logic lines were written. Instead of writing "a = (dist <= olddist) * (c1)", I wrote "a = (c1) * (dist <= olddist)". Thus, the order of the expressions seems to make a difference, even when care is taken to make the choices mutually exclusive, and even when assignments are performed *after* the comparisons are made.
A possible hint about what is happening is provided by "ghostless-B". This formula is just like "ghost", except for the addition of the line "a = b = 0" just before the lines with the conditional logic. With this line added, the formula makes the same images as "ghostless-A".
The meaning of this (it seems to me) is that in the "ghost" formula, if "dist <= olddist" is FALSE, 'a' doesn't always get set to zero as I would expect. My guess is 'a' just keeps the old value from the previous iteration.
It has been suggested that floating-point optimizations may be behind this odd behavior. If any reader knows more, please contact me. But in the meantime, this appears to reinforce Chuck Ebbert's suggestion that when using conditional logic we should write the comparison expression after the '*'.
Finally, I should point out that this "ghosting" effect is quite unusual in my experience; it's *not* an everyday problem. I believe it is aggravated by the formula's complexity. Let me illustrate:
ghostless-C { ;Yet another solution -- simplify! z = c1 = pixel, c2 = fn1(pixel), olddist = 100 tgt = fn2(pixel), rt = real(tgt), it = imag(tgt): x = real(z) - rt y = imag(z) - it dist = (x * x) + (y * y) a = (dist <= olddist) * (c1) b = (olddist < dist) * (c2) olddist = dist z = z*z + a + b |z| <= 4 }I simplified the logic of "ghost" and by doing this, I was able to eliminate three variables: oldx, oldy, and oldz. This formula has no ghosting problem, even though the conditional statements are the same as in "ghost". And because it is simpler, it is also faster!
So here we have a problem with five different solutions: setting FN2() to IDENT, using integer math, changing the order of expressions, re-initializing variables, and simplifying formula logic. Very peculiar...
Although I don't yet know the real cause of the problem, I have drawn some lessons from the experience: If a formula gets too complicated, things might go haywire. When the parser starts behaving strangely, try rewriting the formula in a different way. And above all, look for ways to simplify your formulas.
One of the amazing things about exploring a fractal is that you can continue to zoom deeper and deeper into the image, and yet you keep seeing new details. Similarly, the study of fractals and formulas appears to offer an inexhaustible supply of new things to learn. This document has only scratched the surface.
Here are a few suggestions on how you can increase your knowledge.
Complex math lies at the heart of the fractals produced by the parser. If you have no understanding of the math, I don't see how you can really understand a formula. So find a good teacher and take a class. Read a textbook or FRACTAL CREATIONS. Practice complex arithmetic with a paper and pencil. But don't skip over the subject, and don't be overwhelmed by the word "complex"; if you can learn algebra, you can learn about complex numbers.
It bears repeating that a formula is a program. The parser language, although comparatively small, is sometimes more obscure than most modern programming languages; so if you have never learned another programming language, doing so would probably be very helpful to you. Although the details vary from one language to another, there are common concepts that will help you understand Fractint formulas. If you become familiar with the main concepts (variables, looping, assignment and comparison, conditional execution, etc.) in a setting that makes these concepts easier to understand, that experience will help you understand the programming aspects of a formula.
You may not find this subject taught in your school yet, but there are some excellent books that can help you learn more.
The best introductory book for a Fractint user (in my opinion) is still FRACTAL CREATIONS by Timothy Wegner and Bert Tyler. The second edition of the book also includes a CD with some terrific images.
James Gleick's CHAOS: MAKING A NEW SCIENCE is a fascinating introduction to the related notions of chaos, non-linear dynamics and fractals. It discusses how and why these concepts came into being, and gives glimpses into the minds of some fascinating people.
CHAOS AND FRACTALS: NEW FRONTIERS OF SCIENCE by Peitgen, Jurgens and Saupe is practically an encyclopedia of fractals; it's full of history, good illustrations, illuminating discussions of the math, and sample programs.
THE FRACTAL GEOMETRY OF NATURE by Benoit Mandelbrot is the book that started it all, but be aware that it is *not* for the mathematical dilettante; neither is FRACTALS EVERYWHERE by Michael Barnsley. Both books are very highly regarded as standard works in the fractal library, however.
Clifford Pickover has written several books that discuss fractals, including COMPUTERS, PATTERN, CHAOS AND BEAUTY and MAZES FOR THE MIND. These books are jammed with great pictures, stimulating ideas, program listings and pseudo-code, and much more. Dr. Pickover also edits the journal COMPUTERS AND GRAPHICS, which includes articles on fractal graphics.
There are a few other periodicals that deal with fractals. AMYGDALA is published in the US, while the diskette-based FRAC'CETERA comes to us from the UK. Both of these publications are warmly endorsed by their readers.
When you're struggling to learn new concepts, it's wonderful to have a knowledgeable friend who'll help you. If you don't know anyone who fits that description, get a modem and go online. You'll be able to meet fellow fractal nuts on CompuServe, for example, in the GRAPHDEV area. Look for fractal discussions on the Internet or on your favorite online service or BBS, and if nobody is talking about the subject, bring it up yourself. We're a small minority, but our ranks are constantly growing. For years I created fractal images by and for myself, and I'm here to tell you that it's a *lot* more fun when you meet people with whom you can share your questions and accomplishments.
That's all for this iteration of the file. I hope you found it to be interesting and instructive. There's no doubt in my mind that it can be improved. If you have any insights or suggestions that will make it better, please share them with me so I can share them with everyone else.
Back to
The Fractint Home Page.
or back to
The Fractint Index Page.